/**
* Canada Visualizer Page
* Interactive map showing federal seat counts by party per province/territory
* Also supports equalization visualization with step-by-step explainer
*/
'use client';
import React, { useMemo } from 'react';
import { useTranslations, useLocale } from 'next-intl';
import { Header } from '@/components/Header';
import { Footer } from '@/components/Footer';
import { ShareButton } from '@/components/ShareButton';
import {
CanadaMap,
SeatLegend,
DataTable,
VisualizationSelector,
EqualizationCompactView,
EqualizationGuidancePanel,
EqualizationInlineLegend,
EqualizationDataPanel,
SeatCountGuidancePanel,
SeatCountDataPanel,
} from '@/components/visualizer';
import { useVisualizerStore } from '@/hooks/useVisualizerStore';
import { SeatDataProvider, useSeatDataContext } from '@/contexts/SeatDataContext';
import { EqualizationDataProvider, useEqualizationData } from '@/contexts/EqualizationDataContext';
import { provinceCodes } from '@/lib/visualizer/provinceData';
import { Tag } from 'lucide-react';
/**
* Seat Count visualization content
* Three-column layout on desktop: Guidance | Map | Data
* Stacked layout on mobile
*/
function SeatCountContent() {
const t = useTranslations('visualizer');
const locale = useLocale() as 'en' | 'fr';
const { showLabels, toggleLabels, visualizationType, selectedParty } = useVisualizerStore();
const { getProvinceSeatData } = useSeatDataContext();
const shareData = useMemo(() => {
const baseUrl = typeof window !== 'undefined' ? window.location.origin : '';
return {
url: `${baseUrl}/${locale}/visualizer?view=${visualizationType}`,
title: locale === 'en' ? 'Canada Federal Seat Distribution' : 'Répartition des sièges fédéraux du Canada',
description: locale === 'en'
? 'Interactive map showing federal seat counts by party per province/territory'
: 'Carte interactive montrant le nombre de sièges fédéraux par parti par province/territoire',
};
}, [locale, visualizationType]);
// Generate label data for selected party's seats per province
const partyLabelData = useMemo(() => {
if (!selectedParty) return undefined;
const labelData: Record<string, number | undefined> = {};
for (const code of provinceCodes) {
const provinceData = getProvinceSeatData(code);
if (provinceData?.seats) {
const seats = provinceData.seats[selectedParty] || 0;
labelData[code] = seats > 0 ? seats : undefined;
}
}
return labelData;
}, [selectedParty, getProvinceSeatData]);
// Format label value - just show the number
const formatLabelValue = (value: string | number | undefined) => {
if (value === undefined || value === null || value === 0) return '';
return String(value);
};
return (
<div className="h-screen flex flex-col bg-bg-primary overflow-hidden">
<Header />
<main className="flex-1 flex flex-col min-h-0">
{/* Compact Header */}
<div className="flex-shrink-0 px-4 py-2 border-b border-border-subtle">
<div className="flex items-center justify-between max-w-[1800px] mx-auto">
<div className="flex items-center gap-3">
<VisualizationSelector />
<h1 className="text-lg font-semibold text-text-primary hidden sm:block">
{locale === 'en' ? 'Seat Distribution' : 'Répartition des sièges'}
{selectedParty && (
<span className="text-sm font-normal text-text-secondary ml-2">
— {selectedParty}
</span>
)}
</h1>
</div>
<div className="flex items-center gap-2">
<button
onClick={toggleLabels}
className={`
flex items-center gap-1.5 px-2.5 py-1.5 rounded-md text-sm font-medium transition-colors
${showLabels
? 'bg-accent-red text-white'
: 'bg-bg-secondary text-text-secondary hover:bg-bg-elevated hover:text-text-primary'
}
`}
>
<Tag className="w-4 h-4" />
<span className="hidden sm:inline">{t('labels') || 'Labels'}</span>
</button>
<ShareButton
url={shareData.url}
title={shareData.title}
description={shareData.description}
variant="icon"
size="md"
/>
</div>
</div>
</div>
{/* Mobile Layout */}
<div className="flex-1 flex flex-col min-h-0 xl:hidden overflow-hidden">
{/* Map Section - constrained height */}
<div className="flex-shrink-0 h-[45vh] min-h-[200px] p-2 pb-0 overflow-hidden">
<CanadaMap
labelData={partyLabelData}
formatLabelValue={formatLabelValue}
/>
</div>
{/* Legend - separate fixed section */}
<div className="flex-shrink-0 px-2 py-1 bg-bg-primary">
<SeatLegend showCounts={true} />
</div>
{/* Data Table */}
<div className="flex-1 min-h-0 border-t border-border-subtle bg-bg-secondary overflow-y-auto">
<div className="p-3">
<DataTable />
</div>
</div>
</div>
{/* Desktop Layout - Three columns */}
<div className="hidden xl:flex flex-1 min-h-0 max-w-[1800px] mx-auto w-full">
{/* Left Panel - Guidance */}
<div className="w-[320px] flex-shrink-0 border-r border-border-subtle bg-bg-secondary overflow-y-auto">
<div className="p-4">
<SeatCountGuidancePanel />
</div>
</div>
{/* Center - Large Map */}
<div className="flex-1 min-w-0 p-4 flex flex-col">
<div className="flex-1 min-h-0 w-full">
<CanadaMap
labelData={partyLabelData}
formatLabelValue={formatLabelValue}
/>
</div>
{/* Inline Legend below map */}
<div className="flex-shrink-0 pt-3">
<SeatLegend showCounts={true} />
</div>
</div>
{/* Right Panel - Data */}
<div className="w-[340px] flex-shrink-0 border-l border-border-subtle bg-bg-secondary overflow-hidden flex flex-col">
<div className="p-4 flex-1 min-h-0 flex flex-col">
<SeatCountDataPanel />
</div>
</div>
</div>
{/* Compact Footer */}
<div className="flex-shrink-0 px-4 py-1.5 text-center text-xs text-text-tertiary border-t border-border-subtle">
{t('dataSource')}
</div>
</main>
</div>
);
}
/**
* Equalization visualization content - has access to EqualizationDataContext
* Three-column layout on desktop: Guidance | Map | Data
* Stacked layout on mobile with compact view
*/
function EqualizationContent() {
const t = useTranslations('visualizer');
const tEq = useTranslations('equalization');
const locale = useLocale() as 'en' | 'fr';
const { showLabels, toggleLabels, visualizationType } = useVisualizerStore();
const { data, isProvinceTerritorial } = useEqualizationData();
// Get equalization label data for map labels
const equalizationLabelData = useMemo(() => {
const labelData: Record<string, number | undefined> = {};
for (const code of Object.keys(data)) {
if (!isProvinceTerritorial(code) && data[code].paymentMillions > 0) {
labelData[code] = data[code].paymentMillions;
}
}
return labelData;
}, [data, isProvinceTerritorial]);
const shareData = useMemo(() => {
const baseUrl = typeof window !== 'undefined' ? window.location.origin : '';
return {
url: `${baseUrl}/${locale}/visualizer?view=${visualizationType}`,
title: locale === 'en' ? 'Canada Equalization Payments Visualizer' : 'Visualiseur des paiements de péréquation du Canada',
description: locale === 'en'
? 'Interactive visualization of federal equalization payments across Canadian provinces'
: 'Visualisation interactive des paiements de péréquation fédéraux entre les provinces canadiennes',
};
}, [locale, visualizationType]);
const formatLabelValue = (value: string | number | undefined, _code: string) => {
if (value === undefined || value === null) return '';
if (typeof value === 'number' && value > 0) {
return locale === 'en' ? `$${(value / 1000).toFixed(1)}B` : `${(value / 1000).toFixed(1)} G$`;
}
return '';
};
return (
<div className="h-screen flex flex-col bg-bg-primary overflow-hidden">
<Header />
<main className="flex-1 flex flex-col min-h-0">
{/* Compact Header */}
<div className="flex-shrink-0 px-4 py-2 border-b border-border-subtle">
<div className="flex items-center justify-between max-w-[1800px] mx-auto">
<div className="flex items-center gap-3">
<VisualizationSelector />
<h1 className="text-lg font-semibold text-text-primary hidden sm:block">
{locale === 'en' ? 'Equalization' : 'Péréquation'}
</h1>
</div>
<div className="flex items-center gap-2">
<button
onClick={toggleLabels}
className={`
flex items-center gap-1.5 px-2.5 py-1.5 rounded-md text-sm font-medium transition-colors
${showLabels
? 'bg-accent-red text-white'
: 'bg-bg-secondary text-text-secondary hover:bg-bg-elevated hover:text-text-primary'
}
`}
>
<Tag className="w-4 h-4" />
<span className="hidden sm:inline">{t('labels') || 'Labels'}</span>
</button>
<ShareButton
url={shareData.url}
title={shareData.title}
description={shareData.description}
variant="icon"
size="md"
/>
</div>
</div>
</div>
{/* Desktop: Three-column layout | Mobile: Stacked with compact view */}
{/* Mobile Layout */}
<div className="flex-1 flex flex-col min-h-0 xl:hidden overflow-hidden">
{/* Map Section - constrained height */}
<div className="flex-shrink-0 h-[45vh] min-h-[200px] p-2 pb-0 overflow-hidden">
<CanadaMap
labelData={equalizationLabelData}
formatLabelValue={formatLabelValue}
/>
</div>
{/* Compact Data Panel */}
<div className="flex-1 min-h-0 border-t border-border-subtle bg-bg-secondary">
<div className="h-full p-3 overflow-auto">
<EqualizationCompactView />
</div>
</div>
</div>
{/* Desktop Layout - Three columns */}
<div className="hidden xl:flex flex-1 min-h-0 max-w-[1800px] mx-auto w-full">
{/* Left Panel - Guidance */}
<div className="w-[320px] flex-shrink-0 border-r border-border-subtle bg-bg-secondary flex flex-col">
<div className="p-4 flex-1 min-h-0 flex flex-col overflow-hidden">
<EqualizationGuidancePanel />
</div>
</div>
{/* Center - Large Map */}
<div className="flex-1 min-w-0 p-4 flex flex-col">
<div className="flex-1 min-h-0 w-full">
<CanadaMap
labelData={equalizationLabelData}
formatLabelValue={formatLabelValue}
/>
</div>
{/* Inline Legend below map */}
<div className="flex-shrink-0 pt-3">
<EqualizationInlineLegend />
</div>
</div>
{/* Right Panel - Data */}
<div className="w-[340px] flex-shrink-0 border-l border-border-subtle bg-bg-secondary overflow-hidden flex flex-col">
<div className="p-4 flex-1 min-h-0 flex flex-col">
<EqualizationDataPanel />
</div>
</div>
</div>
{/* Compact Footer */}
<div className="flex-shrink-0 px-4 py-1.5 text-center text-xs text-text-tertiary border-t border-border-subtle">
{tEq('dataSource')}
</div>
</main>
</div>
);
}
export default function VisualizerPage() {
const { visualizationType } = useVisualizerStore();
const isEqualizationMode = visualizationType === 'equalization';
// Render the appropriate content based on visualization mode
// Each has its own provider requirements
if (isEqualizationMode) {
return (
<SeatDataProvider>
<EqualizationDataProvider>
<EqualizationContent />
</EqualizationDataProvider>
</SeatDataProvider>
);
}
return (
<SeatDataProvider>
<SeatCountContent />
</SeatDataProvider>
);
}